﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;


namespace IntelHexParser
{
	/// <summary>
	/// Main class for the IntelHexParser project.  Use the static Parse method to create an instance of the HexFile class.
	/// </summary>
	public class HexFile
	{
		#region Properties
		/// <summary>
		/// The Start Linear Address to apply to each line if not null.
		/// </summary>
		public byte[] StartLinearAddress { get; set; }


		/// <summary>
		/// The collection of SegmentBlocks that make up this Hex file.
		/// </summary>
		public List<SegmentBlock> Blocks { get; set; }


		/// <summary>
		/// The fully parsed Lines into Hex Records.
		/// </summary>
		public List<HexRecord> Records { get; set; }
		#endregion


		private HexFile()
		{
			this.Blocks = new List<SegmentBlock>();
		}


		#region Main Static Parse Method
		/// <summary>
		/// Main method of the IntelHexFile Parser.  Call this to create a HexFile class instance.
		/// </summary>
		/// <param name="filename">Full filename and path of hex file to parse.</param>
		/// <param name="maximumLineSize">Typical file uses a maximum of 255 bytes per line. (Default is 255).  Can be 255 or lower.</param>
		/// <param name="gapFillerByte">Byte (represented as a two character string) to use when there is a gap between the starting address of a line and the previous line's ending address.  (Default is "FF".)  Only used if gap plus existing line data is less than or equal to maximumLineSize.</param>
		/// <param name="verifyEachChecksum">Boolean value to indicate if the hax file checksums should be verified when parsing.  (Default is false.)</param>
		/// <param name="doFullParse">Boolean value to indicate if returned Hex File object includes fully parsed line records.  If false then Records collection is null.  (Default is false.)</param>
		/// <returns></returns>
		public static HexFile Parse(string filename, byte maximumLineSize = 255, string gapFillerByte = "FF", bool verifyEachChecksum = false, bool doFullParse = false)
		{
			if (string.IsNullOrEmpty(filename) || !File.Exists(filename))
			{
				throw new ApplicationException("Invalid or missing Filename.");
			}

			byte[] starting_address = new byte[4];
			int num_message_bytes = 0; // number of bytes in a message
			bool firstBlockLine = true;
			bool eofHit = false;
			int lineSize = 0;
			int lineNum = 0;
			int address = 0;
			int prevAddr = 0;
			int prevSize = 0;
			byte sa1 = 0x00;
			byte sa0 = 0x00;
			SegmentBlock currentBlock = null;

			HexFile file = new HexFile();
			if (doFullParse)
			{
				file.Records = new List<HexRecord>();
			}

			try
			{
				string filler = gapFillerByte.ToUpper();
				List<string> lines = GetLinesFromFile(filename);

				//read all lines, finding each block and the size each of each block in bytes
				foreach (string line in lines)
				{
					if (eofHit)
					{
						break;
					}

					string thisLine = line.Remove(0, 1); // remove the colon from the front of the record

					if (verifyEachChecksum)
					{
						if (!lineChecksumIsGood(thisLine))
						{
							throw new ApplicationException(string.Format("Line {0} failed checksum verifcation.", lineNum));
						}
					}

					if (doFullParse)
					{

					}

					int recordType = (int)Convert.ToByte(thisLine.Substring(6, 2), 16);

					if (firstBlockLine)	//this is first line of file
					{
						currentBlock = new SegmentBlock();
						starting_address[3] = 0;
						starting_address[2] = 0;
						starting_address[1] = Convert.ToByte(thisLine.Substring(2, 2), 16);
						starting_address[0] = Convert.ToByte(thisLine.Substring(4, 2), 16);

						currentBlock.StartingAddress = starting_address;
					}

					switch (recordType)
					{
						case 0:	//data record
							//get addr and size for this line
							lineSize = (int)Convert.ToByte(thisLine.Substring(0, 2), 16);
							address = Convert.ToInt16(thisLine.Substring(2, 4), 16);

							//check against previous line
							if (!firstBlockLine)
							{
								if (prevAddr + prevSize < address)
								{
									//insert "FF" for each missing byte
									int prevNum = lineNum - 1;
									int fillerSize = address - (prevAddr + prevSize);

									if (prevSize + fillerSize > maximumLineSize)
									{
										//simulate by inserting a new block when size of gap is greater than line size.
										currentBlock.Size = num_message_bytes;
										file.Blocks.Add(currentBlock);

										sa1 = currentBlock.StartingAddress[3];
										sa0 = currentBlock.StartingAddress[2];

										currentBlock = new SegmentBlock();
										starting_address = new byte[4];
										starting_address[3] = sa1;
										starting_address[2] = sa0;
										starting_address[1] = Convert.ToByte(thisLine.Substring(2, 2), 16);
										starting_address[0] = Convert.ToByte(thisLine.Substring(4, 2), 16);

										currentBlock.StartingAddress = starting_address;
										num_message_bytes = 0;
										lineNum = 0;
									}
									else
									{
										num_message_bytes += fillerSize;
										currentBlock.Lines[prevNum] = (prevSize + fillerSize).ToString("X2") + currentBlock.Lines[prevNum].Substring(2, currentBlock.Lines[prevNum].Length - 4) + GetFiller(fillerSize, filler) + currentBlock.Lines[prevNum].Substring(currentBlock.Lines[prevNum].Length - 2);
									}
								}
							}
							else
							{
								firstBlockLine = false;
							}
							prevAddr = address;
							prevSize = lineSize;
							lineNum++;

							num_message_bytes = num_message_bytes + lineSize;
							currentBlock.Lines.Add(thisLine);
							break;

						case 1:	//end of file, do nothing but exit loop
							currentBlock.Size = num_message_bytes;
							file.Blocks.Add(currentBlock);
							eofHit = true;
							break;

						case 2:	//extended segment address
							//create a new block setting the Extended Segment property to this 02 address
							currentBlock.Size = num_message_bytes;
							file.Blocks.Add(currentBlock);

							sa1 = currentBlock.StartingAddress[3];
							sa0 = currentBlock.StartingAddress[2];

							currentBlock = new SegmentBlock();
							currentBlock.ExtendedSegmentAddress = thisLine.Substring(8, 4);

							starting_address = new byte[4];
							starting_address[3] = sa1;
							starting_address[2] = sa0;
							starting_address[1] = Convert.ToByte(thisLine.Substring(2, 2), 16);
							starting_address[0] = Convert.ToByte(thisLine.Substring(4, 2), 16);

							currentBlock.StartingAddress = starting_address;
							num_message_bytes = 0;
							lineNum = 0;
							break;

						case 3:	//Start segment address
							byte cs1 = 0;
							byte cs2 = 0;
							byte ip1 = 0;
							byte ip2 = 0;

							currentBlock.StartSegmentAddress = new byte[4] { cs1, cs2, ip1, ip2 };
							break;

						case 4:	//start of new block
							currentBlock.Size = num_message_bytes;
							file.Blocks.Add(currentBlock);

							currentBlock = new SegmentBlock();
							starting_address = new byte[4];
							starting_address[3] = Convert.ToByte(thisLine.Substring(8, 2), 16);
							starting_address[2] = Convert.ToByte(thisLine.Substring(10, 2), 16);
							starting_address[1] = 0;
							starting_address[0] = 0;

							currentBlock.StartingAddress = starting_address;
							num_message_bytes = 0;
							lineNum = 0;
							break;

						case 5:	//Start Linear Address
							byte[] eip = new byte[4];
							eip[0] = 0;
							eip[1] = 0;
							eip[2] = 0;
							eip[3] = 0;
							file.StartLinearAddress = eip;
							break;
					}
				}

				if (!file.Blocks.Contains(currentBlock))
				{
					//add last block
					currentBlock.Size = num_message_bytes;
					file.Blocks.Add(currentBlock);
				}
			}
			catch (Exception ex)
			{

			}

			return file;
		}
		#endregion


		#region Private Static Helper Methods
		private static bool lineChecksumIsGood(string line)
		{
			//convert every two characters to a single byte (except the checksum byte), add them together.  if the lower 8-bits = the checksum byte, we are good
			int checksum = 0;

			for (int i = 0; i < line.Length - 2; i += 2)
			{
				//add this byte to our checksum
				byte data = Convert.ToByte(line.Substring(i, 2), 16);
				checksum = checksum + data;
			}

			byte lowerByte = (byte)checksum;
			byte lineChecksum = Convert.ToByte(line.Substring(line.Length - 2), 16);

			return (lowerByte == lineChecksum);
		}


		private static List<string> GetLinesFromFile(string fileName)
		{
			List<string> lines = new List<string>();
			try
			{
				StreamReader sr = new StreamReader(fileName);
				lines.AddRange(sr.ReadToEnd().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries));
			}
			catch (Exception e)
			{
				throw;
			}
			return lines;
		}


		private static string GetFiller(int numMissingBytes, string filler)
		{
			StringBuilder sb = new StringBuilder();

			for (int i = 0; i < numMissingBytes; i++)
			{
				sb.Append(filler);
			}

			return sb.ToString();
		}
		#endregion
	}
}
